Ce tutorial vous guide dans la transformation de code "en vrac" vers une architecture organisée avec des classes statiques. Vous apprendrez à créer des fichiers de classe et à refactoriser du code existant.
Vous avez développé une course d'escargots avec des fonctions préfixées (Snail_Draw, Race_DrawTrack, etc.). Ce code fonctionne, mais il est temps de le professionnaliser en utilisant des classes statiques.
À travers ce tutorial, vous apprendrez à :
using pour importer des namespacesVoici une version simplifiée de la course d'escargots avec des fonctions préfixées. Ce sera le point de départ.
Si un projet avec une course d'escargots fonctionnelle n'est pas disponible, créer un nouveau projet Console App nommé SnailRaceRefactoring et copier ce code dans Program.cs (ou adapter le code existant selon l'exemple) :
// ========== DONNÉES GLOBALES ==========
int numberOfSnails = 3;
int screenWidth = 50;
int startX = 2;
int finishX = screenWidth - 3;
int[] snailX = new int[3];
int[] snailY = new int[3];
int[] snailEnergy = new int[3];
string[] snailNames = { "Turbo", "Speedy", "Flash" };
ConsoleColor[] snailColors = { ConsoleColor.Yellow, ConsoleColor.Cyan, ConsoleColor.Magenta };
Random random = new Random();
// ========== PROGRAMME PRINCIPAL ==========
Console.Clear();
Console.CursorVisible = false;
Race_Init();
Race_DrawTrack();
Race_DrawAllSnails();
Console.SetCursorPosition(0, 12);
Console.WriteLine("Appuyez sur ENTER pour lancer la course !");
Console.ReadLine();
// Boucle de course
bool raceFinished = false;
int winner = -1;
while (!raceFinished)
{
for (int i = 0; i < numberOfSnails; i++)
{
if (random.Next(100) < snailEnergy[i])
{
Snail_Clear(snailX[i], snailY[i]);
snailX[i]++;
Snail_Draw(snailX[i], snailY[i], snailColors[i]);
snailEnergy[i] -= random.Next(1, 4);
if (snailEnergy[i] < 10) snailEnergy[i] = 10;
}
if (snailX[i] >= finishX)
{
winner = i;
raceFinished = true;
break;
}
}
Race_ShowEnergy();
Thread.Sleep(100);
}
Console.SetCursorPosition(0, 14);
Console.ForegroundColor = snailColors[winner];
Console.WriteLine($"{snailNames[winner]} a gagné la course !");
Console.ResetColor();
Console.ReadKey();
// ========== FONCTIONS RACE_ ==========
void Race_Init()
{
for (int i = 0; i < numberOfSnails; i++)
{
snailX[i] = startX;
snailY[i] = 2 + i * 2;
snailEnergy[i] = 100;
}
}
void Race_DrawTrack()
{
for (int i = 0; i < numberOfSnails; i++)
{
int y = 2 + i * 2;
Console.SetCursorPosition(startX - 1, y);
Console.ForegroundColor = ConsoleColor.White;
Console.Write("|");
for (int x = startX; x < finishX; x++)
{
Console.SetCursorPosition(x, y);
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.Write(".");
}
Console.SetCursorPosition(finishX, y);
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("|");
}
Console.ResetColor();
}
void Race_DrawAllSnails()
{
for (int i = 0; i < numberOfSnails; i++)
{
Snail_Draw(snailX[i], snailY[i], snailColors[i]);
}
}
void Race_ShowEnergy()
{
Console.SetCursorPosition(0, 10);
for (int i = 0; i < numberOfSnails; i++)
{
Console.ForegroundColor = snailColors[i];
string bar = new string('#', snailEnergy[i] / 10);
string empty = new string('-', 10 - snailEnergy[i] / 10);
Console.Write($"{snailNames[i],6}[{bar}{empty}] ");
}
Console.ResetColor();
}
// ========== FONCTIONS SNAIL_ ==========
void Snail_Draw(int x, int y, ConsoleColor color)
{
Console.SetCursorPosition(x, y);
Console.ForegroundColor = color;
Console.Write("@");
Console.ResetColor();
}
void Snail_Clear(int x, int y)
{
Console.SetCursorPosition(x, y);
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.Write(".");
Console.ResetColor();
}
Lancer l'application pour vérifier que tout fonctionne.
On remarque que les fonctions sont organisées avec des préfixes :
|
| Gestion de la course |
|
| Affichage des escargots |
On remarque aussi que les variables globales (snailX, snailY, snailEnergy, snailNames, snailColors, etc.) sont utilisées principalement par les fonctions Race_*. Ces variables et ces fonctions forment un groupe logique qui devrait être réuni.
Transformer les fonctions Snail_* en méthodes d'une classe statique Snail.
Dans l'IDE :
Snail.cs
L'IDE crée un fichier avec ce contenu par défaut :
namespace SnailRaceRefactoring
{
internal class Snail
{
}
}
Modifier le fichier Snail.cs comme suit :
namespace SnailRaceRefactoring
{
static class Snail
{
public static void Draw(int x, int y, ConsoleColor color)
{
Console.SetCursorPosition(x, y);
Console.ForegroundColor = color;
Console.Write("@");
Console.ResetColor();
}
public static void Clear(int x, int y)
{
Console.SetCursorPosition(x, y);
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.Write(".");
Console.ResetColor();
}
}
}
|
|
Fonction "en vrac" dans Program.cs | Méthode dans la classe |
Appel : | Appel : |
Remarque : la classe Snail ne contient ici que des méthodes (pas de variables/champs). C'est normal : toutes les données des escargots (snailX, snailNames, etc.) sont liées à la course et seront déplacées dans la classe Race à l'étape suivante.
static class : La classe ne peut pas être instanciée (pas de new Snail())public : Le membre (méthode ou champ) est accessible depuis l'extérieur de la classestatic : Le membre appartient à la classe, pas à une instanceDans Program.cs, remplacer les appels aux anciennes fonctions :
Avant :
Snail_Draw(snailX[i], snailY[i], snailColors[i]);
Snail_Clear(snailX[i], snailY[i]);
Après :
Snail.Draw(snailX[i], snailY[i], snailColors[i]);
Snail.Clear(snailX[i], snailY[i]);
Supprimer les fonctions Snail_Draw et Snail_Clear de Program.cs (elles sont maintenant dans Snail.cs).
Compiler et exécuter. Le programme doit fonctionner exactement comme avant !
Transformer les fonctions Race_* en méthodes d'une classe statique Race.
Contrairement à Snail (qui n'avait que des méthodes), les fonctions Race_* utilisent des variables globales :
numberOfSnails, screenWidth, startX, finishXsnailX[], snailY[], snailEnergy[], snailNames[], snailColors[]Ces variables et ces fonctions forment un groupe logique : elles doivent être regroupées dans la classe Race. Les variables globales deviennent des champs statiques (static) de la classe.
Race.csnamespace SnailRaceRefactoring
{
static class Race
{
// ========== DONNÉES DE LA COURSE ==========
public static int NumberOfSnails = 3;
public static int ScreenWidth = 50;
public static int StartX = 2;
public static int FinishX = ScreenWidth - 3;
public static int[] SnailX = new int[3];
public static int[] SnailY = new int[3];
public static int[] SnailEnergy = new int[3];
public static string[] SnailNames = { "Turbo", "Speedy", "Flash" };
public static ConsoleColor[] SnailColors = {
ConsoleColor.Yellow,
ConsoleColor.Cyan,
ConsoleColor.Magenta
};
// ========== MÉTHODES ==========
public static void Init()
{
for (int i = 0; i < NumberOfSnails; i++)
{
SnailX[i] = StartX;
SnailY[i] = 2 + i * 2;
SnailEnergy[i] = 100;
}
}
public static void DrawTrack()
{
for (int i = 0; i < NumberOfSnails; i++)
{
int y = 2 + i * 2;
Console.SetCursorPosition(StartX - 1, y);
Console.ForegroundColor = ConsoleColor.White;
Console.Write("|");
for (int x = StartX; x < FinishX; x++)
{
Console.SetCursorPosition(x, y);
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.Write(".");
}
Console.SetCursorPosition(FinishX, y);
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("|");
}
Console.ResetColor();
}
public static void DrawAllSnails()
{
for (int i = 0; i < NumberOfSnails; i++)
{
Snail.Draw(SnailX[i], SnailY[i], SnailColors[i]);
}
}
public static void ShowEnergy()
{
Console.SetCursorPosition(0, 10);
for (int i = 0; i < NumberOfSnails; i++)
{
Console.ForegroundColor = SnailColors[i];
string bar = new string('#', SnailEnergy[i] / 10);
string empty = new string('-', 10 - SnailEnergy[i] / 10);
Console.Write($"{SnailNames[i],6}[{bar}{empty}] ");
}
Console.ResetColor();
}
}
}
snailX, numberOfSnails, etc.) sont maintenant des champs public static de la classe. Elles sont rangées avec les méthodes qui les utilisentSnailX[i] sans préfixe). Depuis l'extérieur, on écrit Race.SnailX[i]DrawAllSnails() appelle Snail.Draw() - les classes peuvent s'utiliser mutuellement !Champ public statique | PascalCase |
|
Méthode publique | PascalCase |
|
Variable locale | camelCase |
|
Simplifier Program.cs en utilisant les nouvelles classes.
Remplacer tout le contenu de Program.cs par :
// Programme principal - Course d'escargots
Console.Clear();
Console.CursorVisible = false;
Race.Init();
Race.DrawTrack();
Race.DrawAllSnails();
Console.SetCursorPosition(0, 12);
Console.WriteLine("Appuyez sur ENTER pour lancer la course !");
Console.ReadLine();
// Boucle de course
Random random = new Random();
bool raceFinished = false;
int winner = -1;
while (!raceFinished)
{
for (int i = 0; i < Race.NumberOfSnails; i++)
{
if (random.Next(100) < Race.SnailEnergy[i])
{
Snail.Clear(Race.SnailX[i], Race.SnailY[i]);
Race.SnailX[i]++;
Snail.Draw(Race.SnailX[i], Race.SnailY[i], Race.SnailColors[i]);
Race.SnailEnergy[i] -= random.Next(1, 4);
if (Race.SnailEnergy[i] < 10) Race.SnailEnergy[i] = 10;
}
if (Race.SnailX[i] >= Race.FinishX)
{
winner = i;
raceFinished = true;
break;
}
}
Race.ShowEnergy();
Thread.Sleep(100);
}
Console.SetCursorPosition(0, 14);
Console.ForegroundColor = Race.SnailColors[winner];
Console.WriteLine($"{Race.SnailNames[winner]} a gagné la course !");
Console.ResetColor();
Console.ReadKey();
|
| Variable → Champ |
|
| Fonction → Méthode |
|
| Fonction → Méthode |
|
| Accès qualifié |
Fonctions et variables en vrac | Organisées ensemble dans des classes | Regroupement logique |
Le projet devrait maintenant ressembler à ceci :
SnailRaceRefactoring/
├── Program.cs // Programme principal (simplifié)
├── Race.cs // Classe Race (données + logique de course)
└── Snail.cs // Classe Snail (affichage escargot)
Compiler et exécuter. Le programme doit fonctionner exactement comme avant !
Imaginons un autre projet avec une classe Race pour des courses de voitures. Il y aurait un conflit de noms !
Les namespaces permettent d'organiser les classes et d'éviter ces conflits.
Actuellement, l'IDE a créé le namespace SnailRaceRefactoring. Organisons mieux avec un sous-namespace.
namespace SnailRaceRefactoring.Game
{
static class Snail
{
public static void Draw(int x, int y, ConsoleColor color)
{
Console.SetCursorPosition(x, y);
Console.ForegroundColor = color;
Console.Write("@");
Console.ResetColor();
}
public static void Clear(int x, int y)
{
Console.SetCursorPosition(x, y);
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.Write(".");
Console.ResetColor();
}
}
}
namespace SnailRaceRefactoring.Game
{
static class Race
{
// ... tout le contenu reste identique ...
}
}
Après ces modifications, Program.cs affiche des erreurs :
CS0103: The name 'Race' does not exist in the current context
CS0103: The name 'Snail' does not exist in the current context
usingAjouter cette ligne au début de Program.cs :
using SnailRaceRefactoring.Game;
// Programme principal - Course d'escargots
Console.Clear();
Console.CursorVisible = false;
Race.Init();
// ... reste du code ...
Sans using, il faudrait écrire le nom complet partout :
// Sans using : nom complet obligatoire
SnailRaceRefactoring.Game.Race.Init();
SnailRaceRefactoring.Game.Snail.Draw(x, y, color);
C'est long et répétitif ! Le using permet d'utiliser les noms courts.
Compiler et exécuter. Tout doit fonctionner !
À la compilation, un avertissement (warning) peut apparaître :
IDE0130: Namespace "SnailRaceRefactoring.Game" does not match folder structure,
expected "SnailRaceRefactoring"
En .NET, il existe une convention importante :
Le namespace doit correspondre à l'arborescence des dossiers du projet.
|
|
|
|
|
|
Pour respecter cette convention :
GameSnail.cs et Race.cs dans ce dossierSnailRaceRefactoring/
├── Program.cs // namespace: SnailRaceRefactoring (ou aucun)
└── Game/
├── Race.cs // namespace: SnailRaceRefactoring.Game
└── Snail.cs // namespace: SnailRaceRefactoring.Game
Navigation facile | Le dossier reflète l'organisation logique |
Pas d'avertissements | L'IDE ne se plaint plus |
Standard de l'industrie | Tous les développeurs .NET suivent cette convention |
Refactoring automatique | L'IDE peut renommer le namespace si on déplace un fichier |
Lors de la création d'une classe dans un dossier, l'IDE propose automatiquement le bon namespace :
/Game/ → namespace SnailRaceRefactoring.Game/Display/ → namespace SnailRaceRefactoring.DisplayCompiler le projet. Les avertissements IDE0130 devraient avoir disparu.
Créer une classe utilitaire pour simplifier l'affichage coloré dans la console.
Dans notre code, nous avons souvent ce pattern :
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Texte vert");
Console.ResetColor();
Trois lignes pour afficher du texte coloré, c'est beaucoup !
Comme pour Game, créons un dossier pour le namespace Display :
DisplayDisplay > Ajouter > Classe...SuperConsole.csL'IDE propose automatiquement le namespace SnailRaceRefactoring.Display.
Modifier le contenu :
namespace SnailRaceRefactoring.Display
{
static class SuperConsole
{
/// <summary>
/// Affiche du texte coloré avec retour à la ligne
/// </summary>
public static void WriteLineColor(string text, ConsoleColor color)
{
Console.ForegroundColor = color;
Console.WriteLine(text);
Console.ResetColor();
}
/// <summary>
/// Affiche du texte coloré sans retour à la ligne
/// </summary>
public static void WriteColor(string text, ConsoleColor color)
{
Console.ForegroundColor = color;
Console.Write(text);
Console.ResetColor();
}
/// <summary>
/// Affiche un message de succès (vert)
/// </summary>
public static void WriteSuccess(string text)
{
WriteLineColor(text, ConsoleColor.Green);
}
/// <summary>
/// Affiche un message d'erreur (rouge)
/// </summary>
public static void WriteError(string text)
{
WriteLineColor(text, ConsoleColor.Red);
}
/// <summary>
/// Affiche un message d'avertissement (jaune)
/// </summary>
public static void WriteWarning(string text)
{
WriteLineColor(text, ConsoleColor.Yellow);
}
/// <summary>
/// Affiche du texte à une position précise
/// </summary>
public static void WriteAt(int x, int y, string text)
{
Console.SetCursorPosition(x, y);
Console.Write(text);
}
/// <summary>
/// Affiche du texte coloré à une position précise
/// </summary>
public static void WriteAtColor(int x, int y, string text, ConsoleColor color)
{
Console.SetCursorPosition(x, y);
Console.ForegroundColor = color;
Console.Write(text);
Console.ResetColor();
}
}
}
SuperConsole est dans SnailRaceRefactoring.Display, pas dans SnailRaceRefactoring.Game. C'est une classe d'affichage générique, pas spécifique au jeu.
Modifier Program.cs pour utiliser la nouvelle classe :
using SnailRaceRefactoring.Game;
using SnailRaceRefactoring.Display;
// Programme principal - Course d'escargots
Console.Clear();
Console.CursorVisible = false;
Race.Init();
Race.DrawTrack();
Race.DrawAllSnails();
SuperConsole.WriteAt(0, 12, "Appuyez sur ENTER pour lancer la course !");
Console.ReadLine();
// ... boucle de course ...
// À la fin, remplacer l'affichage du gagnant :
SuperConsole.WriteAt(0, 14, "");
SuperConsole.WriteLineColor($"{Race.SnailNames[winner]} a gagné la course !", Race.SnailColors[winner]);
Console.ReadKey();
SnailRaceRefactoring/
├── Program.cs // Programme principal
├── Game/
│ ├── Race.cs // namespace SnailRaceRefactoring.Game
│ └── Snail.cs // namespace SnailRaceRefactoring.Game
└── Display/
└── SuperConsole.cs // namespace SnailRaceRefactoring.Display
Améliorer la classe Snail en utilisant SuperConsole.
Snail.cs, ajouter le using nécessaireSuperConsoleusing SnailRaceRefactoring.Display;
namespace SnailRaceRefactoring.Game
{
static class Snail
{
public static void Draw(int x, int y, ConsoleColor color)
{
SuperConsole.WriteAtColor(x, y, "@", color);
}
public static void Clear(int x, int y)
{
SuperConsole.WriteAtColor(x, y, ".", ConsoleColor.DarkGray);
}
}
}
4 lignes pour | 1 ligne avec |
Code dupliqué (couleur + reset) | Logique centralisée dans |
Appliquer le même principe à la classe Race pour utiliser SuperConsole dans DrawTrack() et ShowEnergy().
CS0103: The name 'SuperConsole' does not exist in the current context
Cause : Le using du namespace est manquant.
Solution : Ajouter using SnailRaceRefactoring.Display; en haut du fichier.
using SnailRaceRefactoring.Display.SuperConsole; // ERREUR!
CS0138: A 'using' directive can only be applied to namespaces
Cause : using s'applique aux namespaces, pas aux classes.
Solution : Utiliser le namespace : using SnailRaceRefactoring.Display;
CS0122: 'Snail.Draw(...)' is inaccessible due to its protection level
Cause : La méthode n'est pas public.
Solution : Ajouter public devant la méthode : public static void Draw(...)
Snail s = new Snail(); // ERREUR si Snail est static class
CS0712: Cannot create an instance of the static class 'Snail'
Cause : On ne peut pas instancier une classe statique.
Solution : Appeler directement sur la classe : Snail.Draw(...)
using SnailRaceRefactoring; // N'importe PAS Game ni Display !
Race.Init(); // ERREUR: Race non trouvée
Cause : using SnailRaceRefactoring n'importe pas automatiquement les sous-namespaces.
Solution : Importer explicitement : using SnailRaceRefactoring.Game;
C# permet d'importer les membres statiques d'une classe avec using static :
using static System.Console;
using static SnailRaceRefactoring.Display.SuperConsole;
// Plus besoin de préfixe !
Clear(); // au lieu de Console.Clear()
CursorVisible = false; // au lieu de Console.CursorVisible
WriteSuccess("Partie terminée !"); // au lieu de SuperConsole.WriteSuccess()
Effacer l'écran |
|
|
Afficher un succès |
|
|
Code plus court | Moins explicite (d'où vient |
Moins de répétition | Risque de confusion entre méthodes |
Classe statique | Conteneur pour champs (données) et méthodes (actions), sans instanciation |
Champ statique | Variable qui appartient à la classe (ex: |
Méthode statique | Fonction qui appartient à la classe (ex: |
public | Rend un membre (champ ou méthode) accessible depuis l'extérieur |
namespace | Organise les classes et évite les conflits de noms |
using | Importe un namespace pour utiliser les noms courts |
Dossiers = NS | Les dossiers doivent refléter la structure des namespaces |
Fonctions préfixées |
|
|
Classe statique |
|
|
Avec namespace |
| champs dans la classe, dans le NS |
Avec using |
|
|
SnailRaceRefactoring/
├── Program.cs // Point d'entrée
├── Game/
│ ├── Race.cs // Logique de course
│ └── Snail.cs // Affichage escargot
└── Display/
└── SuperConsole.cs // Utilitaires console
Vous utilisiez déjà des classes statiques sans le savoir :
|
|
|
|
|
|
Dans le prochain tutorial, vous découvrirez les classes non statiques qui permettent de créer des instances (objets) avec leurs propres données. Chaque escargot deviendra un objet Snail avec ses propres coordonnées et énergie !